package Clases;

import java.net.*;
import java.io.*;
import java.util.Dictionary;
import java.util.Hashtable;

public class Servidor {

    private Direccion direccion;

    public Direccion Direccion() {
        return direccion;
    }
    private Balde balde;

    public Servidor(int id, String ip, int puertoRecibir) {
        balde = new Balde(id);
        direccion = new Direccion(id, ip, puertoRecibir, balde.getLevel(), balde.getSplit());
    }

//    public Servidor(Direccion dir) {
//        balde = new Balde(dir.ID());
//        direccion = dir;
//    }
    void run() {
        try {
            ServerSocket socketServer = new ServerSocket();
            socketServer.bind(new InetSocketAddress(direccion.IP(), direccion.PuertoRecibir()));

            System.out.println("Server " + direccion.ID() + " Escuchando en: "
                    + direccion.IP() + ":" + direccion.PuertoRecibir());

            Socket client = null;
            BufferedReader lectorDeSockets = null;
            String linea = "";

            Boolean sigueEjecucion = true;
            while (linea != null && !linea.equalsIgnoreCase("quit") && sigueEjecucion) {
                client = socketServer.accept();
                lectorDeSockets = new BufferedReader(new InputStreamReader(client.getInputStream()));
                linea = lectorDeSockets.readLine();
                sigueEjecucion = determinarResponsable(linea, lectorDeSockets);
            }
            lectorDeSockets.close();
            client.close();
            socketServer.close();

        } catch (BindException bex) {
            System.out.println("El puerto ya está en uso");
        } catch (Exception ex) {
            System.out.println("Ha ocurrido un error");
        }
    }

    private PrintWriter enviarRespuestaACliente(String respuesta, Direccion dirCliente,
            PrintWriter writer) {
        try {
            Socket respuestaACliente = new Socket();
            respuestaACliente.connect(new InetSocketAddress(dirCliente.IP(),
                    dirCliente.PuertoRecibir()));

            writer = new PrintWriter(respuestaACliente.getOutputStream());
            writer.write(respuesta + "\n");
            writer.flush();
            respuestaACliente.close();
            return writer;

        } catch (ConnectException cex) {
            System.out.println("Error de conexión");
            return null;
        } catch (Exception ex) {
            System.out.println("Ha ocurrido un error");
            return null;
        }
    }

    /**
     * Método encargado de procesar la consulta. Si encuentra la respuesta se la envía al cliente,
     * sino solicita ayuda a otro servidor.
     */
    private void procesarConsulta(String linea, BufferedReader lectorDeSockets, ProcesaConsultas.Operaciones op) {
        try {
            String respuestaProcesamiento = "";
            ProcesaConsultas procesa = new ProcesaConsultas();
            String mensajeDireccionCliente = "";
            Direccion direccionRespuesta = null;
            if (op.equals(ProcesaConsultas.Operaciones.SELECT)) {
                respuestaProcesamiento = procesa.consultar(linea, op);
            } else {
                if (op.equals(ProcesaConsultas.Operaciones.SCAN)) {
                    respuestaProcesamiento = procesa.scanear(linea, op);
                } else {
                    respuestaProcesamiento = procesa.procesar(linea, op);
                }
            }
            System.out.println("Consulta: " + linea + " --> " + respuestaProcesamiento);
            mensajeDireccionCliente = lectorDeSockets.readLine();
            direccionRespuesta = Parser.parsearDireccionCliente(mensajeDireccionCliente);
            PrintWriter writer = null;
            writer = enviarRespuestaACliente(respuestaProcesamiento, direccionRespuesta, writer);
            System.out.println("Se ha enviado la respuesta al cliente " + direccionRespuesta.ID());
            if (direccionRespuesta.Level() != this.direccion.Level()) {
                writer = enviarMensajeDeAjusteACliente(direccionRespuesta, writer);

                System.out.println("Se ha enviado el mensaje de ajuste al cliente "
                        + direccionRespuesta.ID());
            } else {
                System.out.println("No se ha enviado el mensaje de ajuste al cliente "
                        + direccionRespuesta.ID() + " pues tienen en mismo level");
            }
            writer.close();
        } catch (Exception ex) {
            System.out.println("Ha ocurrido un error mientras se procesaba la consulta");
        }
    }

    /**
     * Método encargado de procesar el scan y mandar su respuesta al cliente,
     *
     */
    private void procesarScan(String linea, Direccion direccionRespuesta, ProcesaConsultas.Operaciones op) {
        try {
            String respuestaProcesamiento = "";
            ProcesaConsultas procesa = new ProcesaConsultas();
            if (op.equals(ProcesaConsultas.Operaciones.SELECT)) {
                respuestaProcesamiento = procesa.consultar(linea, op);
            } else {
                if (op.equals(ProcesaConsultas.Operaciones.SCAN)) {
                    respuestaProcesamiento = procesa.scanear(linea, op);
                } else {
                    respuestaProcesamiento = procesa.procesar(linea, op);
                }
            }
            System.out.println("Consulta: " + linea + " --> " + respuestaProcesamiento);
            PrintWriter writer = null;
            writer = enviarRespuestaACliente(respuestaProcesamiento, direccionRespuesta, writer);
            System.out.println("Se ha enviado la respuesta al cliente " + direccionRespuesta.ID());
            if (direccionRespuesta.Level() != this.direccion.Level()) {
                writer = enviarMensajeDeAjusteACliente(direccionRespuesta, writer);

                System.out.println("Se ha enviado el mensaje de ajuste al cliente "
                        + direccionRespuesta.ID());
            } else {
                System.out.println("No se ha enviado el mensaje de ajuste al cliente "
                        + direccionRespuesta.ID() + " pues tienen en mismo level");
            }
            writer.close();
        } catch (Exception ex) {
            System.out.println("Ha ocurrido un error mientras se procesaba la consulta");
        }
    }

    private Boolean determinarResponsable(String linea, BufferedReader lectorDeSockets) {
        Boolean hayQueDerivar = true;
        if (linea.contains("FW_")) {
            linea = linea.replace("FW_", "");
        } else {
            if (linea.contains("FWS_")) {
                linea = linea.replace("FWS_", "");
                hayQueDerivar = false;
            }

        }

        ProcesaConsultas.Operaciones op = ProcesaConsultas.obtenerOperacion(linea);

        if (op.equals(ProcesaConsultas.Operaciones.SCAN)) {
            try {
                String mensajeDireccionCliente = lectorDeSockets.readLine();
                Direccion direccionCliente = Parser.parsearDireccionCliente(mensajeDireccionCliente);
                procesarScan(linea, direccionCliente, op);

                if (hayQueDerivar) {
                    for (Direccion dir : balde.BaldesConocidos()) {
                        int idBalde = dir.ID();
                        derivarConsultaScan(idBalde, linea, direccionCliente);
                    }
                }

            } catch (Exception ex) {
                return false;
            }
            return true;
        } else {
            int rid = Parser.getRID(linea);
            int idBalde = calcularIDBalde(rid);
            if (idBalde == balde.getId()) {
                procesarConsulta(linea, lectorDeSockets, op);
                return true;
            } else {
                return derivarConsulta(idBalde, linea, lectorDeSockets);
            }
        }
    }

    private int calcularHash(int rID) {
        //Operación implementada: h = RID % 2**(level + 1)
        int hash = rID % (int) Math.pow(2, balde.getLevel() + 1);
        return hash;
    }

    private int calcularIDBalde(int rID) {
        int idBalde = rID % (int) Math.pow(2, balde.getLevel());
        if (idBalde < balde.getSplit()) {
            idBalde = calcularHash(rID);
        }
        return idBalde;
    }

    /**
     * Método encargado de derivar la consulta a otro servider
     * @param ipCliente que tiene que ser enviada al otro servidor para que le responda al cliente.
     * @param puertoCliente. Puerto que está escuchando el cliente.
     * @param consulta. la consulta a ser respondida por el otro servidor.
     */
    private Boolean derivarConsulta(int ridServidor, String consulta, BufferedReader lectorDeSockets) {
        Direccion baldeAReenviar = null;
        try {
            //Debería abrir socket contra el otro servidor.
            Socket socketDerivacion = new Socket();
            baldeAReenviar = balde.devolverBaldeConocido(ridServidor);


            String mensajeDireccionCliente = lectorDeSockets.readLine();
            Direccion direccionCliente = Parser.parsearDireccionCliente(mensajeDireccionCliente);

            socketDerivacion.connect(new InetSocketAddress(baldeAReenviar.IP(), baldeAReenviar.PuertoRecibir()));
            String line = "FW_" + consulta;
            PrintWriter writer = null;
            writer = escribirSocket(socketDerivacion, line, writer); //escribe la query

            String datosCliente = "#" + direccionCliente.ID() + ";"
                    + direccionCliente.IP() + ";" + direccionCliente.PuertoRecibir() + ";"
                    + direccionCliente.Level() + ";" + direccionCliente.Split();
            writer = escribirSocket(socketDerivacion, datosCliente, writer); //escribe los datos del client

            writer.close();
            socketDerivacion.close();
            System.out.println("Se ha derivado la consulta " + consulta + " al servidor " + baldeAReenviar.ID());
            return true;
        } catch (ConnectException e) {
            System.out.println("No se puede derivar la consulta porque el servidor " + baldeAReenviar.ID() + " no está levantado");
            return true;
        } catch (Exception e) {
            System.out.println("Ha ocurrido un error mientras se derivaba la consulta");
            return false;
        }
    }

    /**
     * Método encargado de derivar la consulta del scan a otro servider
     * @param ipCliente que tiene que ser enviada al otro servidor para que le responda al cliente.
     * @param puertoCliente. Puerto que está escuchando el cliente.
     * @param consulta. la consulta a ser respondida por el otro servidor.
     */
    private Boolean derivarConsultaScan(int ridServidor, String consulta, Direccion direccionCliente) {
        Direccion baldeAReenviar = null;
        try {
            //Debería abrir socket contra el otro servidor.
            Socket socketDerivacion = new Socket();
            baldeAReenviar = balde.devolverBaldeConocido(ridServidor);

            socketDerivacion.connect(new InetSocketAddress(baldeAReenviar.IP(), baldeAReenviar.PuertoRecibir()));
            String line = "FWS_" + consulta;
            PrintWriter writer = null;
            writer = escribirSocket(socketDerivacion, line, writer); //escribe la query

            String datosCliente = "#" + direccionCliente.ID() + ";"
                    + direccionCliente.IP() + ";" + direccionCliente.PuertoRecibir() + ";"
                    + direccionCliente.Level() + ";" + direccionCliente.Split();
            writer = escribirSocket(socketDerivacion, datosCliente, writer); //escribe los datos del client

            writer.close();
            socketDerivacion.close();
            System.out.println("Se ha derivado la consulta " + consulta + " al servidor " + baldeAReenviar.ID());
            return true;
        } catch (ConnectException e) {
            System.out.println("No se puede derivar la consulta porque el servidor " + baldeAReenviar.ID() + " no está levantado");
            return true;
        } catch (Exception e) {
            System.out.println("Ha ocurrido un error mientras se derivaba la consulta");
            return false;
        }
    }

    private PrintWriter escribirSocket(Socket serverADerivar, String line, PrintWriter writer) {
        try {
            writer = new PrintWriter(serverADerivar.getOutputStream());  //Escribirá los comandos en el socket
            writer.write(line + "\n");
            writer.flush();
            return writer;
        } catch (Exception ex) {
            System.out.println("Ha ocurrido un error al escribir el socket");
            return null;
        }
    }

    private PrintWriter enviarMensajeDeAjusteACliente(Direccion direccionRespuesta, PrintWriter writer) {
        try {
            String mensajeAjuste = "UPD_"
                    + balde.getLevel() + "_"
                    + balde.getSplit() + "_"
                    + generarListaParaMsjAjuste();
            return enviarRespuestaACliente(mensajeAjuste, direccionRespuesta, writer);
        } catch (Exception exc) {
            System.out.println("Ha ocurrido un error al enviar el mensaje de ajuste al cliente");
            return null;
        }
    }

    private String generarListaParaMsjAjuste() {
        String lista = "";
        for (Direccion dir : balde.BaldesConocidos()) {
            lista += dir.ID() + ","
                    + dir.IP() + ","
                    + dir.PuertoRecibir() + ";";
        }
        lista = lista.substring(0, lista.length() - 1);
        return lista;
    }
}
